""" Module with tests for S3D.Decorators.* modules """
from S3D.Decorators import *
from S3D.Decorators.Automaton import *
from unittest import TestCase

class Tests(TestCase):
    def testSingleton(self):
        @singleton
        class MyClass:
            def __new__(cls, test):
                cls.x = 10
                return object.__new__(cls)

            def __init__(self, test):
                test.assertTrue(hasattr(self, 'x'))
                test.assertEquals(self.x, 10)
                self.x = 15

        c = MyClass(self)
        self.assertEqual(c.x, 15)
        c.x = 20
        self.assertEqual(MyClass().x, 20)

        @singleton
        class MyClassWithoutNew:
            def __init__(self):
                self.x = 10

        c = MyClassWithoutNew()
        self.assertEqual(c.x, 10)
        c.x = 20
        self.assertEqual(MyClassWithoutNew().x, 20)

    def testDebug(self):
        global ENABLE_DEBUG_MODE, ENABLE_RETAIL_MODE

        oldDebugMode, oldRetailMode = ENABLE_DEBUG_MODE, ENABLE_RETAIL_MODE

        try:
            @debug
            def test():
                return 10

            @debug_defval(15)
            def test2():
                return 5

            @debug_defval(15, 10)
            def test3():
                return 7

            enableRetailMode(False)
            enableDebugMode()
            self.assertEqual(test(), 10)
            self.assertEqual(test2(), 5)
            self.assertEqual(test3(), 7)
            enableDebugMode(False)
            self.assertIsNone(test())
            self.assertEqual(test2(), 15)
            self.assertEqual(test3(), (15, 10))

            enableRetailMode()
            enableDebugMode()

            @debug
            def test():
                return 10

            @debug_defval(5)
            def test2():
                return 15

            self.assertIsNone(test())
            self.assertEqual(test2(), 5)

        finally:
            enableRetailMode(oldRetailMode)
            enableDebugMode(oldDebugMode)

    def testMethodOf(self):
        class SomeClass:
            pass

        @method_of(SomeClass)
        def test1(self):
            return '%s.test1' % type(self).__name__

        @method_of(SomeClass)
        @staticmethod
        def test2():
            return 'test2'

        @method_of(SomeClass)
        @classmethod
        def test3(cls):
            return '%s.test3' % cls.__name__

        @method_of(SomeClass, 'namingTest')
        @staticmethod
        def test4():
            return 'test4'

        p = SomeClass()
        self.assertEqual(p.test1(), 'SomeClass.test1')
        self.assertEqual(p.test2(), 'test2')
        self.assertEqual(p.test3(), 'SomeClass.test3')
        self.assertEqual(p.namingTest(), 'test4')

    def testFunctor(self):
        @functor
        def test(a, b, c):
            return a + b * c

        self.assertEqual(test(1, 2, 3)(), 7)
        self.assertEqual(test(1, 2)(3), 7)
        self.assertEqual(test(1)(2, 3), 7)
        self.assertEqual(test()(1, 2, 3), 7)

        @functor_append
        def test2(a, b, c):
            return a + b * c

        self.assertEqual(test2()(1, 2, 3), 7)
        self.assertEqual(test2(3)(1, 2), 7)
        self.assertEqual(test2(2, 3)(1), 7)
        self.assertEqual(test2(1, 2, 3)(), 7)

        try:
            test2(1, 2, 3)(4)
            self.fail()
        except TypeError:
            pass

        self.assertEqual(test2(a = 3, b = 2, c = 3)(), 9)
        self.assertEqual(test2(b = 2, c = 3)(3), 9)
        self.assertEqual(test2(a = 3, b = 2, c = 3)(a = 10), 16)

        try:
            test2(a = 3, b = 2, c = 3)(a = 10, d = 3)
            self.fail()
        except TypeError:
            pass

    def testDecorator_Func(self):
        @decorator
        def testDecor(func, name = None):
            return name or extractName(func)

        self.assertEqual(testDecor.__name__, 'testDecor')
        self.assertEqual(testDecor.__module__, Tests.__module__)

        @testDecor
        def test1():
            pass

        self.assertEqual(test1, 'test1')

        @testDecor('customName')
        def test2():
            pass

        self.assertEqual(test2, 'customName')

    def testDecorator_Class(self):
        @decorator
        class testDecor2:
            def __init__(self, name = None):
                self.name = name

            def __call__(self, func):
                return self.name or extractName(func)

        self.assertEqual(testDecor2.__name__, 'testDecor2')
        self.assertEqual(testDecor2.__module__, Tests.__module__)

        @testDecor2
        def test3():
            pass

        self.assertEqual(test3, 'test3')

        @testDecor2('customName2')
        def test4():
            pass

        self.assertEqual(test4, 'customName2')

    def testApply(self):
        @apply
        def test():
            return "test"

        self.assertEqual(test, 'test')

        @apply(1, 2)
        def test2(a, b):
            return a + b

        self.assertEqual(test2, 3)

class TestConstraints(TestCase):
    @staticmethod
    def checkFail(func, *args, _exc = AssertionError, **kw):
        try:
            func(*args, **kw)
            return '<Fail>'
        except _exc as e:
            assert e.args[0] == 'Constraint failed'
            return e.args[1]

    def testConstraints_Simple(self):
        @constraints
        def test(x : int) -> str:
            return str(x + 5)

        self.assertEquals(test(10), '15')

        self.assertEquals(self.checkFail(test, 10.0),       'x')
        self.assertEquals(self.checkFail(test, 'string'),   'x')

    def testConstraints_Return(self):
        @constraints
        def badFunc() -> str:
            return 10

        self.assertEquals(self.checkFail(badFunc), 'return')

    def testConstraints_PartialArgs(self):
        @constraints
        def partialArgs(a : int, b, c : list):
            return list(zip([b for _ in range(a)], c))

        self.assertEqual(partialArgs(3, 'test', [1, 2, 3]), [('test', 1), ('test', 2), ('test', 3)])
        self.assertEquals(self.checkFail(partialArgs, 'string', 10, []), 'a')
        self.assertEquals(self.checkFail(partialArgs, 10, (1, 2, 3), 'string'), 'c')

        self.assertEqual(partialArgs(a = 3, b = 'test', c = [1, 2, 3]), [('test', 1), ('test', 2), ('test', 3)])
        self.assertEqual(self.checkFail(partialArgs, a = 'string', b = 10, c = []), 'a')
        self.assertEqual(self.checkFail(partialArgs, a = 10, b = (1, 2, 3), c = 'string'), 'c')

    def testConstraints_ArgsAndKw(self):
        @constraints
        def testArgsWithKw(u, v, *args : int, x : list) -> str:
            return ''.join([a * b for a, b in zip(args, x)])

        self.assertEqual(testArgsWithKw('u', 'v', 1, 2, 3, x = ['a', 'b', 'c']), 'abbccc')
        self.assertEqual(self.checkFail(testArgsWithKw, 1, 2, 3, x = 'string'), 'x')
        self.assertEqual(self.checkFail(testArgsWithKw, 1, 2, 'c', x = [1, 2, 3]), 'args')
        self.assertEqual(self.checkFail(testArgsWithKw, 1, 2, 'c', x = 'str'), 'args')

    def testConstraints_Methods(self):
        class MyClass:
            @staticmethod
            @constraints
            def test(a : int, b : int) -> int:
                return a + b

            @constraints
            def testFail(self) -> int:
                return "fail"

        c = MyClass()
        self.assertEqual(c.test(1, 2), 3)
        self.assertEqual(self.checkFail(c.test, 'a', 'b'), 'a')
        self.assertEqual(self.checkFail(c.testFail), 'return')

    def testConstraints_Constr(self):
        @constraints
        def test(x : [int, float], *args : [ctr_call(), ctr_has('__call__')]) -> (int, int):
            return 1, 2

        test(10, ctr_call(), constr())

        @constraints
        def test2(x : ctr_not(int, float)) -> str:
            return str(x)

        test2('ehhl')
        self.assertEqual(self.checkFail(test2, 10), 'x')
        test2((10, 10.5))

    def testConstraints_Tuple(self):
        @constraints
        def test(x: (int, ([int, float], constr([int], lambda x: len(x) > 5)))) -> int:
            return x[0]

        self.assertEqual(self.checkFail(test, (10, (0.5, [1, 2, 3, 4, 5]))), 'x')
        test((10, (15, [p * 2 for p in range(10)])))

        @constraints
        def make_tuple(*args : int) -> (int, int):
            return args

        self.assertEqual(self.checkFail(make_tuple, 1, 2, 'str'), 'args')
        self.assertEqual(self.checkFail(make_tuple, 1, 2, 3), 'return')
        make_tuple(1, 2)

    def testCheckConstr(self):
        def check(val, cons, res = True):
            c = constr(cons)
            self.assertTrue(c(val) == res)

        check(10, int)
        check(10, float, False)
        check(10, [int, float])
        check(10, [float, str], False)

        check([1, 2, 3], [int])
        check([1, 2, 'str'], [int], False)
        check([1, 2, 'str'], [[int, str]])
        check([1, 'str', [1, 2]], [])
        check([1, 'str', [1, 2]], [int], False)
        check([1, 'str', [1, 2]], [int, str], False)
        check([1, 'str', [1, 2]], [[int, str, []]])

        check((1, 2, 3), ())
        check((1, 2, 3), (int, int, int))
        check((1, 2, 3), (int, int, [int, float]))
        check((1, 2, 3), (int, int), False)
        check((1, 2, 3), (int, int, float), False)

        check([1, 2, 3], [lambda x: x >= 1 and x <= 3])
        check([1, 2, 3, 4], [lambda x: x >= 1 and x <= 3], False)

        check([1, 2, 3], [constr(int, range(10))])
        check([1, 2, 3, 4], [constr(int, range(1, 4))], False)
        check([1, 2, '3'], [constr(int, range(1, 4))], False)

        check(1, constr())
        check(None, constr(int, allowNone = True))
        check(None, [None, int, float])

        from collections import namedtuple
        subclass = namedtuple('MyScubclass', 'x, y, z')
        check(subclass(1, 2, 3), ())

        class MyList(list):
            pass

        check(MyList(), [])

        check(int, ctr_type())
        check(int, ctr_type(int))
        check(float, ctr_type(int, float))
        check(str, ctr_type(int, float), False)
        check(10, ctr_type(int, float), False)
        check(10, ctr_type(int), False)
        check(10, ctr_type(), False)
        check([], [str])

class TestAutomaton(TestCase):
    @staticmethod
    @constraints
    def checkFail(func : ctr_call(), exc = AssertionError) -> bool:
        try:
            func()
            return False
        except exc:
            return True

    def testEvent(self, x = None):
        if x is None:
            x = event("x, y, z")

        t = x(1, 2, 3)
        self.assertEqual((t.x, t.y, t.z), (1, 2, 3))

        t = x(x = 1, z = 3, y = 2)
        self.assertEqual((t.x, t.y, t.z), (1, 2, 3))

        self.checkFail(lambda: x(1, 2), (AssertionError, TypeError))
        self.checkFail(lambda: x(1, 2, a = 3), (KeyError, TypeError))
        self.checkFail(lambda: event("a, a"))

    def testCustomEvent(self):
        @event
        def my_event(self, x, y, z):
            self.x = x
            self.y = y
            self.z = z

        self.testEvent(my_event)

    def testEventDescriptor(self):
        class AnAutomaton:
            test = event("")

            def castEvent(self, e):
                pass

        assert isinstance(AnAutomaton.test, type)

        c = AnAutomaton()
        c.test()
        assert isinstance(c.test, EventBind)

    def testAutomatonDecl(self):
        @automaton
        class MyAuto:
            e1 = event("x, y")

        assert hasattr(MyAuto, 'castEvent')
        assert hasattr(MyAuto, 'event_list')
        self.assertEqual(MyAuto.event_list, ('e1', ))

    def testCastEvent(self):
        @automaton
        class MyAuto:
            e1 = event('x, y')

            @event
            def e2(self, x, y):
                self.x, self.y = x, y

            class e3(Event):
                def __init__(self, x, y):
                    self.x, self.y = x, y

        self.assertEqual(MyAuto.event_list, ('e1', 'e2', 'e3'))


        @method_of(MyAuto)
        def castEvent(auto, e):
            nonlocal check
            self.assertEqual(e.x, 10)
            self.assertEqual(e.y, 'test')
            check = True

        a = MyAuto()
        check = False; a.e1(10, 'test');            self.assertTrue(check)
        check = False; a.e1(y = 'test', x = 10);    self.assertTrue(check)
        check = False; a.e2(10, 'test');            self.assertTrue(check)
        check = False; a.e2(y = 'test', x = 10);    self.assertTrue(check)
        check = False; a.e3(10, 'test');            self.assertTrue(check)
        check = False; a.e3(y = 'test', x = 10);    self.assertTrue(check)
        self.assertTrue(check)

    def testStates(self):
        @automaton
        class MyAuto:
            e1 = event()
            e2 = event('info')

            def __init__(self):
                self.x = 10

            @initial
            class DefaultState:
                def onEnter(self):
                    nonlocal check
                    assert self.x == 10
                    check = True

            class OtherState:
                pass

        assert hasattr(MyAuto, 'state_list')
        self.assertEqual(MyAuto.state_list, ('DefaultState', 'OtherState'))

        assert hasattr(MyAuto, 'onInitial')
        assert hasattr(MyAuto, 'onFinish')

        for name in MyAuto.state_list:
            state = getattr(MyAuto, name)
            assert hasattr(state, 'onEnter')
            assert hasattr(state, 'onLeave')

        check = False
        a = MyAuto()
        assert check

        assert not a.isFinished()
        assert a.state == MyAuto.DefaultState
        a.e1()
        a.e2('info')

    def testSwitchAuto(self):
        @automaton
        class SwitchAuto:
            switch_on   = event()
            switch_off  = event()
            shutdown    = event('message')

            def __init__(self):
                self.log = []

            def print(self, text):
                self.log.append(text)

            def onInitial(self):
                self.print('initial')

            def onFinish(self):
                self.print('finish')

            @initial
            class OFF:
                def onEnter(self):
                    self.print('-> OFF')
                def onLeave(self):
                    self.print('<- OFF')

                @on_event('switch_on', 'ON')
                def onSwitchOn(self, e):
                    self.print('switch_on -> ON')

                @default
                def onAny(self, e):
                    self.print(type(e).__name__)

            class ON:
                def onEnter(self):
                    self.print('-> ON')

                @on_event('shutdown')
                def goAway(self, e):
                    self.print('shutdown(%s)' % e.message)
                    self.state = None

        a = SwitchAuto()
        a.switch_off()
        a.switch_on()
        a.shutdown("test")
        self.assertEqual(a.log, [
            'initial',
            '-> OFF',
            'switch_off',
            'switch_on -> ON',
            '<- OFF',
            '-> ON',
            'shutdown(test)',
            #'<- ON', there is no ON.onLeave
            'finish',
        ])

    def testCastEventReturn(self):
        @automaton
        class MyGenerator:
            getval = event()
            select = event('type', prefix = None)

            def onInitial(self):
                self.prefix = None

            @initial
            class B_str:
                @on_event('select', lambda a, e: e.type is int, 'A_str')
                def changeState(self, e):
                    pass

                @on_event('getval')
                def value(self, e):
                    if self.prefix is None:
                        return 'test'
                    return self.prefix + 'test'

            class A_str:
                @on_event('select', ['B_str'])
                def changeState(self, e):
                    if e.type is str:
                        self.state = 'B_str'
                        self.prefix = e.prefix

                @on_event('getval')
                def value(self, e):
                    return 10

        a = MyGenerator()

        self.assertEqual(a.getval(), 'test')
        a.select(int)
        self.assertEqual(a.getval(), 10)
        a.select(str, prefix = 'prefix_')
        self.assertEqual(a.getval(), 'prefix_test')

    def testTransitions(self):
        @automaton
        class MyAuto:
            getval = event(cmd = None)
            switch = event()

            @initial
            class FIRST:
                @on_event('switch', 'SECOND')
                def switchToSecond(self, e):
                    pass

                @on_event('getval', ['SECOND'])
                def onGetVal(self, e):
                    if e.cmd == 'second':
                        self.state = 'SECOND'
                    elif e.cmd == 'third':
                        self.state = 'THIRD'
                    return 'first'

            class SECOND:
                @on_event('getval')
                def onGetVal(self, e):
                    return 'second'

            class THIRD:
                pass

        a = MyAuto()
        assert a.getval() == 'first'
        a.switch()
        assert a.getval() == 'second'

        a = MyAuto()
        assert a.getval() == 'first'
        assert a.getval(cmd = 'second') == 'first'
        assert a.getval() == 'second'

        a = MyAuto()
        self.assertTrue(self.checkFail(lambda: a.getval(cmd = 'third')))

    def testAnnotations(self):
        @event_handler
        def on_test():
            return 10

        assert isinstance(on_test, EventHandler)
        assert on_test.event == 'test'
        assert on_test.cond is None
        assert on_test.handler() == 10
        self.assertEqual(on_test.next, [])

        @event_handler
        def default(self, e : lambda: 'cond_test') -> 'NEXT':
            return self * e

        assert isinstance(default, EventHandler)
        assert default.event is None
        assert default.cond() == 'cond_test'
        assert default.handler('t', 3) == 'ttt'
        assert default.next == 'NEXT'

        try:
            @event_handler
            def on_e() -> lambda: 'fail':
                pass
            check = False
        except AssertionError:
            check = True
        self.assertTrue(check)

    def testInitialsCount(self):
        try:
            @automaton
            class A:
                class STATE_A:
                    pass
                class STATE_B:
                    pass

            check = False
        except AssertionError:
            check = True
        self.assertTrue(check)

        try:
            @automaton
            class B:
                @initial
                class STATE_A:
                    pass
                @initial
                class STATE_B:
                    pass

            check = False
        except AssertionError:
            check = True
        self.assertTrue(check)

        @automaton
        class C:
            class SINGLE_STATE:
                pass
        
    def testAnnotatedAuto(self):
        @automaton
        class Switcher:
            switch = event()
            getState = event()

            @initial
            class STATE_A:
                def on_switch(self, e) -> 'STATE_B':
                    pass
                def on_getState(self, e):
                    return self.state.__name__
                def default(self, e: lambda a, e: e.value > 5):
                    return 'ok'

            class STATE_B:
                def on_switch(self, e) -> 'STATE_A':
                    pass
                def on_getState(self, e):
                    return self.state.__name__
                @default(cond = lambda a, e: e.value < 5, next = 'STATE_A')
                def default_1(self, e):
                    pass

                @default(next = 'STATE_C')
                def default_2(self, e):
                    pass

            class STATE_C:
                def on_getState(self, e):
                    return self.state.__name__
                def default(self, e):
                    self.finish()

        custom = event('value')

        a = Switcher()
        self.assertEqual(a.getState(), 'STATE_A')
        a.switch()
        self.assertEqual(a.getState(), 'STATE_B')
        a.switch()
        self.assertEqual(a.getState(), 'STATE_A')
        self.assertIsNone(a.castEvent(custom(value = 0)))
        self.assertEqual(a.castEvent(custom(value = 10)), 'ok')
        a.switch()
        self.assertEqual(a.getState(), 'STATE_B')
        a.castEvent(custom(0))
        self.assertEqual(a.getState(), 'STATE_A')
        a.switch()
        self.assertEqual(a.getState(), 'STATE_B')
        a.castEvent(custom(10))
        self.assertEqual(a.getState(), 'STATE_C')
        a.switch()
        self.assertTrue(a.isFinished())

    def testDefaultHandlers(self):
        @automaton
        class DefHandler:
            switch = event()
            value = event()

            def default(self, e):
                return 'DefHandler.default'

            @initial
            class A:
                def on_switch(self, e) -> 'B':
                    pass

                def default(self, e):
                    return 'A.default'

            class B:
                def on_switch(self, e) -> None:
                    pass

        c = DefHandler()
        self.assertEqual(c.value(), 'A.default')
        c.switch()
        self.assertEqual(c.value(), 'DefHandler.default')
        c.switch()
        self.assertTrue(c.isFinished())

    def testDefaultHandlers2(self):
        @automaton
        class Switcher:
            switch = event(cmd = None)
            value = event()

            def on_value(self, e):
                return self.state.__name__

            @initial
            class A:
                def on_switch(self, e) -> 'B':
                    if e.cmd == 'finish':
                        self.state = None

            class B:
                def on_switch(self, e) -> 'A':
                    pass

        a = Switcher()
        self.assertEqual(a.value(), 'A')
        a.switch()
        self.assertEqual(a.value(), 'B')
        a.switch()
        self.assertEqual(a.value(), 'A')
        a.switch(cmd = 'finish')
        self.assertTrue(a.isFinished())
